<?php
/**
 * Nuwani-v2 Bot Framework
 *
 * This file is part of the Nuwani v2 Bot Framework, a simple set of PHP classes which allow
 * you to set-up and run your own bot. It features advanced, PHP-5.3 based syntax for
 * optimal performance and security.
 * 
 * Configuration: GameEcho
 * Module: GameEcho
 *
 * The GameEcho configuration directive is an array with all information
 * required for the module required to run. It defines the port we'll be
 * listening to, together with games and keys that are allowed to use
 * this system. That's mainly convenient to stop random people from breaking
 * in to the Nuwani bot system.
 *
 * The AccessList array defines systems which have access, defined by a
 * number of parameters, being:
 *
 *   [Ip]        Ip address of the system that'll send us information.
 *   [Channel]   Channel the bots will output the information to.
 *   [Format]    Filename of the format-description in the Data directory.
 *
 * 
 * 'GameEcho' => array
 * (
 * 	'Port'		=> 38792,
 * 	'AccessList'	=> array
 * 	(
 * 		array
 * 		(
 * 			'Ip'		=> '127.0.0.1',
 * 			'SecurityKey'	=> 'd7b9e37006df55f2484c82609a482a21',
 * 			'Channel'	=> '#LVP.NewEcho',
 * 			'Format'	=> 'GameEcho/LVP.format',
 * 		),
 * 		
 * 	),
 * ),
 *
 * Author: Peter Beverloo
 *         peter@lvp-media.com
 */

require 'Connection.php';

class GameEcho extends ModuleBase
{
	/**
	 * Constant: VERSION
	 *
	 * The version constant will be send to a connecting client to indicate
	 * that we've accepted the connection, asking the client to identify.
	 */
	
	const	VERSION		= 'GameEcho-1.0';
	
	/**
	 * Constant: PACKET_*
	 *
	 * Various packet identifiers which will be used to initialise the
	 * basic communication. It's basically our echo-protocol.
	 */
	
	const	PACKET_IDENTIFY		= 1;
	const	PACKET_DATA		= 2;
	const	PACKET_OUTPUT		= 3;

	/**
	 * Property: m_rListenSocket
	 *
	 * This property contains an array with the listening socket we'll be
	 * using to receive new connections, from servers for example.
	 */
	
	private $m_rListenSocket ;
	
	/**
	 * Property: m_aConfiguration
	 *
	 * The configuration property contains a copy of the configuration as
	 * supplied by the configuration array. That's quite some configuring!
	 */
	
	private $m_aConfiguration ;
	
	/**
	 * Property: m_aRegistered
	 *
	 * An array with the games which have successfully registered themselfes
	 * with the echo system, 
	 */
	
	private $m_aRegistered ;
	
	/**
	 * Function: __construct
	 * 
	 * The constructor will initialise the socket we'll be using for primary
	 * listening, e.g. games can register themselfes on.
	 */
	
	public function __construct ()
	{
		$this -> m_aConfiguration = Configuration :: getInstance () -> get ('GameEcho');
		
		if (empty ($this -> m_aConfiguration)) {
			throw new Exception ('No configuration found for module "GameEcho". This module will not be available.');
		}
		
		$this -> m_rListenSocket = socket_create (AF_INET, SOCK_STREAM, SOL_TCP);
		$this -> m_aRegistered   = array
		(
			'Sockets'	=> array ($this -> m_rListenSocket),
			'Validating'	=> array (),
			'Identified'	=> array ()
		);
		
		socket_set_option   ($this -> m_rListenSocket, SOL_SOCKET, SO_REUSEADDR, 1);
		socket_bind         ($this -> m_rListenSocket, 0, $this -> m_aConfiguration ['Port']);
		socket_listen       ($this -> m_rListenSocket);
		
		echo 'Loaded the GameEcho module..' . NL;
	}
	
	/**
	 * Function: onTick
	 *
	 * The onTick function will be invoked every few milliseconds, allowing us to
	 * handle new connections as well as incoming messages from the connections.
	 */
	
	public function onTick ()
	{
		$aReceive = $this -> m_aRegistered ['Sockets'];
		$aWrite   = NULL;
		$aExcept  = NULL;
		
		if (socket_select ($aReceive, $aWrite, $aExcept, 0) <= 0)
			return ; /** Nothing to do.. **/
		
		if (in_array ($this -> m_rListenSocket, $aReceive)) // new connection;
		{
			$rSocket = socket_accept ($this -> m_rListenSocket);
			$nSocketIdentifier = (int) $rSocket;
			
			socket_getpeername  ($rSocket, $sIpAddress, $nPort);
			socket_write        ($rSocket, self :: VERSION . NL);
			
			echo '[GameEcho] Incoming connection from ' . $sIpAddress . ':' . $nPort . '.' . NL;
			
			$this -> m_aRegistered ['Sockets']    [$nSocketIdentifier] = $rSocket;
			$this -> m_aRegistered ['Validating'] [$nSocketIdentifier] = array
			(
				'Socket'	=> $rSocket,
				'Address'	=> $sIpAddress . ':' . $nPort
			);
		}
		
		// read incoming data from all the other sockets;
		foreach ($aReceive as $pReceivingSocket)
		{
			if ($pReceivingSocket == $this -> m_rListenSocket)
				continue ; /** Ignore the listening socket **/
			
			$mIncoming = @ socket_read ($pReceivingSocket, 1024, PHP_NORMAL_READ);
			$nSocketIdentifier =  (int) $pReceivingSocket;
			
			if ($mIncoming === false)
			{
				unset ($this -> m_aRegistered ['Sockets'] [$nSocketIdentifier]);
				if (isset ($this -> m_aRegistered ['Identified'] [$nSocketIdentifier]))
				{
					$aSocketInfo = $this -> m_aRegistered ['Identified'] [$nSocketIdentifier];
					echo '[GameEcho] Connection with ' . $aSocketInfo ['Address'] . ' has been lost.' . NL; 
					
					unset ($this -> m_aRegistered ['Identified'] [$nSocketIdentifier]);
					continue ;
				}
				else
				{
					$aSocketInfo = $this -> m_aRegistered ['Validating'] [$nSocketIdentifier];
					echo '[GameEcho] Connection with ' . $aSocketInfo ['Address'] . ' has been lost.' . NL; 
					
					unset ($this -> m_aRegistered ['Validating'] [$nSocketIdentifier]);
					continue ;
				}
				
				continue ;
			}
			
			$mIncoming = trim ($mIncoming);
			if (strlen ($mIncoming) <= 3 || $mIncoming [1] != '|')
				continue ;
			
			$nPacketCode     = (int) $mIncoming [0];
			$sPacketContents = trim (substr ($mIncoming, 2));
			
			switch ($nPacketCode)
			{
				case self :: PACKET_IDENTIFY: // identification process
				{
					if (!isset ($this -> m_aRegistered ['Validating'] [$nSocketIdentifier]))
					{
						$this -> forceClose ($nSocketIdentifier);
						continue ;
					}
					
					socket_getpeername  ($pReceivingSocket, $sIpAddress, $nPortNumber);
					foreach ($this -> m_aConfiguration ['AccessList'] as $aGameInfo)
					{
						if ($aGameInfo ['Ip'] != $sIpAddress && $aGameInfo ['Ip'] != '*')
							continue ;
						
						if ($aGameInfo ['SecurityKey'] != $sPacketContents && $aGameInfo ['SecurityKey'] != '*')
							continue ;
						
						$this -> m_aRegistered ['Identified'] [$nSocketIdentifier] = array
						(
							'Control'	=> new GameEcho_Connection ($pReceivingSocket, $aGameInfo),
							'Address'	=> $sIpAddress . ':' . $nPortNumber,
							'Socket'	=> $pReceivingSocket,
							'Connected'	=> time ()
						);
						
						echo '[GameEcho] Connection with ' . $sIpAddress . ':' . $nPortNumber . ' has been identified.' . NL;
						unset ($this -> m_aRegistered ['Validating'] [$nSocketIdentifier]);
					}

					break;
				}
				case self :: PACKET_DATA: // data packet, not for output
				{
					if (!isset ($this -> m_aRegistered ['Identified'] [$nSocketIdentifier]))
						continue ;
					
					$this -> m_aRegistered ['Identified'] [$nSocketIdentifier] ['Control'] -> onData ($sPacketContents);
					break;
				}
				case self :: PACKET_OUTPUT: // output packet
				{
					if (!isset ($this -> m_aRegistered ['Identified'] [$nSocketIdentifier]))
						continue ;
					
					$this -> m_aRegistered ['Identified'] [$nSocketIdentifier] ['Control'] -> onOutput ($sPacketContents);
					break;
				}
			} // end of the switch
		} // for each socket
		
		return true ;
	}
	
	/**
	 * Function: forceClose
	 * Argument: nSocketId (integer) - Id of the socket to close
	 *
	 * If some socket is sending us invalid data, we should immediately close
	 * it without feeling sorry. They might be tryin' to fuck around with us.
	 */
	
	private function forceClose ($nSocketId)
	{
		$sIpAddress  = 'unknown';
		$nPortNumber = 0;
		
		$sKey = isset ($this -> m_aRegistered ['Validating'] [$nSocketId]) ? 'Validating' : 'Identified';
		
		socket_getpeername  ($this -> m_aRegistered [$sKey] [$nSocketId] ['Socket'], $sIpAddress, $nPortNumber);
		unset ($this -> m_aRegistered [$sKey] [$nSocketId]);
		
		// remove the entire socket;
		socket_close ($this -> m_aRegistered ['Sockets'] [$nSocketId]);
		unset ($this -> m_aRegistered ['Sockets'] [$nSocketId]);
		
		// output a message;
		echo '[GameEcho] Connection with ' . $sIpAddress . ':' . $nPortNumber . ' has been closed (invalid packet).' . NL; 
	}

};

?>